import 'package:built_collection/built_collection.dart';
import 'package:backend.core/core/domain/entities/web/web_bank_card_info.dart';
import 'package:backend.core/core/domain/entities/web/web_bank_card_type.dart';
import 'package:backend.core/core/domain/entities/web/web_payment_method.dart';
import 'package:backend.core/core/domain/entities/web/web_payment_method_failure.dart';
import 'package:backend.core/core/domain/entities/web/yookassa/yookassa_bank_card_info.dart';
import 'package:backend.core/core/domain/entities/web/yookassa/yookassa_payment_method.dart';
import 'package:backend.core/core/domain/repositories/web_payment_method_repository/repository.dart';
import 'package:equatable/equatable.dart';
import 'package:mongo_dart/mongo_dart.dart';

part 'dtos/web_bank_card_info_dto.dart';

part 'dtos/web_bank_card_type_dto.dart';

part 'dtos/web_payment_method_dto.dart';

part 'dtos/web_payment_method_dto_factory.dart';

part 'dtos/web_payment_method_failure_dto.dart';

part 'dtos/web_saved_payment_method_info_dto.dart';

part 'dtos/yookassa/yookassa_bank_card_info_dto.dart';

part 'dtos/yookassa/yookassa_payment_method_dto.dart';

const _kPaymentMethodCollectionKey = 'web_payment_methods';
const _kSavedPaymentMethodInfoCollectionKey = 'web_saved_payment_method_infos';

class WebPaymentMethodRepositoryImpl implements WebPaymentMethodRepository {
  const WebPaymentMethodRepositoryImpl({
    required Db db,
  }) : _db = db;

  final Db _db;

  @override
  Future<WebPaymentMethod?> findPaymentMethod({
    required String id,
  }) async {
    final paymentMethodCollection = _db.collection(_kPaymentMethodCollectionKey);

    final data = await paymentMethodCollection.findOne(
      where.eq(_kWebPaymentMethodIdKey, id),
    );

    if (data == null) {
      return null;
    }

    final paymentMethodDto = _WebPaymentMethodDtoFactory().fromJson(data);
    final paymentMethod = paymentMethodDto.toEntity();

    return paymentMethod;
  }

  @override
  Future<BuiltList<WebPaymentMethod>> findUserPaymentMethods({
    required String userId,
  }) async {
    final paymentMethodCollection = _db.collection(_kPaymentMethodCollectionKey);

    final paymentMethodStream = paymentMethodCollection.find(
      where.eq(_kWebPaymentMethodUserIdKey, userId),
    );

    final paymentMethodJsons = await paymentMethodStream.toList();

    final paymentMethods = paymentMethodJsons.map((paymentMethodJson) {
      final paymentMethodDto = _WebPaymentMethodDtoFactory().fromJson(paymentMethodJson);
      return paymentMethodDto.toEntity();
    }).toBuiltList();

    return paymentMethods;
  }

  @override
  Future<BuiltList<WebPaymentMethod>> findAllPaymentMethods() async {
    final paymentMethodCollection = _db.collection(_kPaymentMethodCollectionKey);

    final paymentMethodStream = paymentMethodCollection.find();

    final paymentMethodJsons = await paymentMethodStream.toList();

    final paymentMethods = paymentMethodJsons.map((paymentMethodJson) {
      final paymentMethodDto = _WebPaymentMethodDtoFactory().fromJson(paymentMethodJson);
      return paymentMethodDto.toEntity();
    }).toBuiltList();

    return paymentMethods;
  }

  @override
  Future<BuiltList<WebPaymentMethod>> findSavedUserPaymentMethods({
    required String userId,
  }) async {
    final savedPaymentMethodInfoCollection = _db.collection(_kSavedPaymentMethodInfoCollectionKey);

    final futureResults = await Future.wait([
      savedPaymentMethodInfoCollection.findOne(
        where.eq(_kWebPaymentMethodUserIdKey, userId),
      ),
      findUserPaymentMethods(
        userId: userId,
      ),
    ]);

    var i = 0;
    final savedPaymentMethodInfoJson = futureResults[i++] as Map<String, dynamic>?;
    final paymentMethods = futureResults[i++] as BuiltList<WebPaymentMethod>;

    if (savedPaymentMethodInfoJson == null) {
      return BuiltList();
    }

    final savedPaymentMethodInfo =
        _WebSavedPaymentMethodInfoDto.fromJson(savedPaymentMethodInfoJson);

    final savedPaymentMethods = savedPaymentMethodInfo.savedPaymentMethodIds.map((paymentMethodId) {
      return paymentMethods.firstWhere((paymentMethod) {
        return paymentMethod.id == paymentMethodId;
      });
    }).toBuiltList();

    return savedPaymentMethods;
  }

  @override
  Future<void> upsertPaymentMethod({
    required WebPaymentMethod paymentMethod,
  }) async {
    final paymentMethodCollection = _db.collection(_kPaymentMethodCollectionKey);
    final savedPaymentMethodInfoCollection = _db.collection(_kSavedPaymentMethodInfoCollectionKey);

    final futureResults = await Future.wait([
      findSavedUserPaymentMethods(
        userId: paymentMethod.userId,
      ),
      savedPaymentMethodInfoCollection.findOne(
        where.eq(_kWebPaymentMethodUserIdKey, paymentMethod.userId),
      ),
    ]);

    var i = 0;
    final savedPaymentMethods = futureResults[i++] as BuiltList<WebPaymentMethod>;
    final savedPaymentMethodInfoJson = futureResults[i++] as Map<String, dynamic>?;

    final _WebSavedPaymentMethodInfoDto savedPaymentMethodInfoDto;

    if (savedPaymentMethodInfoJson == null) {
      savedPaymentMethodInfoDto = _WebSavedPaymentMethodInfoDto(
        userId: paymentMethod.userId,
        savedPaymentMethodIds: BuiltList(),
      );
    } else {
      savedPaymentMethodInfoDto =
          _WebSavedPaymentMethodInfoDto.fromJson(savedPaymentMethodInfoJson);
    }

    var updatedSavedPaymentMethodInfoDto = savedPaymentMethodInfoDto;
    var updatedPaymentMethod = paymentMethod;

    if (paymentMethod.saved) {
      final sameSavedPaymentMethodExists = savedPaymentMethods.any((existingSavedPaymentMethod) {
        return existingSavedPaymentMethod.id != paymentMethod.id &&
            existingSavedPaymentMethod.bankCardInfo.cardType ==
                paymentMethod.bankCardInfo.cardType &&
            existingSavedPaymentMethod.bankCardInfo.expirationYear ==
                paymentMethod.bankCardInfo.expirationYear &&
            existingSavedPaymentMethod.bankCardInfo.expirationMonth ==
                paymentMethod.bankCardInfo.expirationMonth &&
            existingSavedPaymentMethod.bankCardInfo.last4 == paymentMethod.bankCardInfo.last4;
      });

      if (sameSavedPaymentMethodExists) {
        if (updatedPaymentMethod is YookassaPaymentMethod) {
          updatedPaymentMethod = updatedPaymentMethod.copyWith(
            saved: () => false,
          );
        } else {
          throw UnimplementedError();
        }
      } else if (!savedPaymentMethodInfoDto.savedPaymentMethodIds.contains(paymentMethod.id)) {
        final updatedSavedPaymentMethodIds =
            savedPaymentMethodInfoDto.savedPaymentMethodIds.rebuild((savedPaymentMethodIds) {
          savedPaymentMethodIds.add(paymentMethod.id);
        });

        updatedSavedPaymentMethodInfoDto = savedPaymentMethodInfoDto.copyWith(
          savedPaymentMethodIds: () => updatedSavedPaymentMethodIds,
        );
      }
    }

    if (!updatedPaymentMethod.saved) {
      if (savedPaymentMethodInfoDto.savedPaymentMethodIds.contains(updatedPaymentMethod.id)) {
        final updatedSavedPaymentMethodIds =
            savedPaymentMethodInfoDto.savedPaymentMethodIds.rebuild((savedPaymentMethodIds) {
          savedPaymentMethodIds.remove(updatedPaymentMethod.id);
        });

        updatedSavedPaymentMethodInfoDto = savedPaymentMethodInfoDto.copyWith(
          savedPaymentMethodIds: () => updatedSavedPaymentMethodIds,
        );
      }
    }

    final updatedSavedPaymentMethodInfoJson = updatedSavedPaymentMethodInfoDto.toJson();

    final updatedPaymentMethodDto = _WebPaymentMethodDtoFactory().fromEntity(updatedPaymentMethod);
    final updatedPaymentMethodJson = updatedPaymentMethodDto.toJson();

    await Future.wait([
      paymentMethodCollection.replaceOne(
        where.eq(_kWebPaymentMethodIdKey, paymentMethod.id),
        updatedPaymentMethodJson,
        upsert: true,
      ),
      savedPaymentMethodInfoCollection.replaceOne(
        where.eq(_kWebSavedPaymentMethodInfoUserIdKey, paymentMethod.userId),
        updatedSavedPaymentMethodInfoJson,
        upsert: true,
      ),
    ]);
  }
}
